币安生态有两条链,其中大家最常用交互的链为BNB chain,这条链由以太坊魔改而成。还有一条链为币安链(Binance Chain),这条链基于 Cosmos SDK 开发而成。由于BNB代币需要在两条链之间流动,因此,BNB chain和 Binance Chain之间有一个币安官方的资产跨链桥,本次受到攻击的就是这个桥。本文会从代码调用角度对这次攻击流程进行详细解析。
攻击是如何发生的?
根据 samczsun(https://twitter.com/samczsun/status/1578167198203289600?s=46&t=Iew2PzcUHQ1ZocbxT0EwdA#/)的分析, 我们看看币安链的攻击怎么发生的。
1)Relayer向BSC 跨链合约发起一笔交易,调用 handlePackage 函数,函数入参数主要为 payload 和 proof。2)跨链合约根据 默克尔证明 验证 payload 的有效性。3)如果 payload 有效, 将 payload 解析为 msgBytes ,并发送至 TokenHub Contract 处理消息。4)TokenHub 合约 处理 msgBytes, 接收者从全零地址接收指定数量的BNB代币。在本次攻击中,攻击者发送了一个无效的payload并伪造其证明proof,该证明通过了步骤2中的Merkle证明验证,并成功从TokenHub合约中盗取了200万美元BNB。具体来说,攻击者通过将一个恶意的叶子节点添加到IAVL树的rangeProof中实现proof的伪造,同时该伪造并未引起其默克尔根的计算结果的改变。值得一提,IAVL树是Cosmos团队设计的一种新的数据结构,其结合了Merkle树和AVL树的优点,且IAVL树叶节点的存在性证明与Merkle证明的过程相似。这种攻击是如何发生的?让我们先来看看跨链合约的调用逻辑。(1) 在 contracts/CrossChain.sol
中, MerkleProof.validateMerkleProof
函数被调用functionhandlePackage(bytes calldatapayload, bytes calldataproof, uint64 height, uint64 packageSequence, uint8 channelId) onlyInitonlyRelayersequenceInOrder(packageSequence, channelId) blockSynced(height) channelSupported(channelId) external {MerkleProof.validateMerkleProof(ILightClient(LIGHT_CLIENT_ADDR).getAppHash(height),generateKey(packageSequence, channelId),
(2) 在 contracts/MerkleProof.sol
中,函数function validateMerkleProof
首先将上述步骤1中的所有入参组装成十六进制字节码,并通过staticcall
调用bnb-chain的0x65预编译合约。functionvalidateMerkleProof(bytes32 appHash, string memorystoreName, bytes memorykey, bytes memoryvalue, bytes memoryproof) internal view returns (bool) { if (appHash == bytes32(0)) { // | storeName | key length | key | value length | value | appHash | proof | // | 32 bytes | 32 bytes | | 32 bytes | | 32 bytes || bytes memory input = new bytes(128+key.length+value.length+proof.length); uint256[1] memory result; /* solium-disable-next-line */ if iszero(staticcall(not(0), 0x65, input, length, result, 0x20)) {} return result[0] == 0x01;
func (c *iavlMerkleProofValidate) Run(input []byte)
被调用, 步骤2的入参被解析并写入变量kvmp
。随后, kvmp.Validate()
被调用。// | payload length | payload |func (c *iavlMerkleProofValidate) Run(input []byte) (result []byte, err error) {if r := recover(); r != nil {err = fmt.Errorf("internal error: %v\n", r)kvmp, err := lightclient.DecodeKeyValueMerkleProof(input[precompileContractInputMetaDataLength:])return nil, fmt.Errorf("invalid merkle proof")result = make([]byte, merkleProofValidateResultLength)binary.BigEndian.PutUint64(result[merkleProofValidateResultLength-uint64TypeLength:], 0x01)
kvmp := &KeyValueMerkleProof{
IAVL树验证预编译合约
(4) 在 github.com/bnb-chain/bsc/core/vm/lightclient/types.go
文件中, 函数 func (kvmp *KeyValueMerkleProof) Validate()
被调用,后续相应调用流程如下首先,跳转到 err := prt.VerifyValue(kvmp.Proof, kvmp.AppHash, kp.String(), kvmp.Value)
.
再跳转到 prt.Verify(proof, root, keypath, [][]byte{value})
随后跳转到 poz.Verify(root, keypath, args)
最后跳转到 args, err = op.Run(args)
值得一提,从第1步开始,BSC开始使用 tendermint相关代码库。func (kvmp*KeyValueMerkleProof) Validate() bool {prt := DefaultProofRuntime()err := prt.VerifyValue(kvmp.Proof, kvmp.AppHash, kp.String(), kvmp.Value)func (prt *ProofRuntime) VerifyValue(proof *tmcrypto.ProofOps, root []byte, keypath string, value []byte) (err error) {return prt.Verify(proof, root, keypath, [][]byte{value})func (prt *ProofRuntime) Verify(proof *tmcrypto.ProofOps, root []byte, keypath string, args [][]byte) (err error) {poz, err := prt.DecodeProof(proof)return fmt.Errorf("decoding proof: %w", err)return poz.Verify(root, keypath, args)func (poz ProofOperators) Verify(root []byte, keypath string, args [][]byte) (err error) {if !bytes.Equal(root, args[0]) {return cmn.NewError("Calculated root hash is invalid: expected %+v but got %+v", root, args[0])
(5) 步骤4中的opz
是由函数 poz, err := prt.DecodeProof(proof)
解码proof
所得,具体解析结果如下:
注意。这些 "操作 "指定了计算IAVL树的Merkle根的方法,并由 func DefaultProofRuntime() (prt *merkle.ProofRuntime)
in github.com/bnb-chain/bsc/core/vm/lightclient/multistoreproof.go
指定
(6) 如上图所示,它将首先跳转到 func (op IAVLValueOp) Run
func (opIAVLValueOp) Run(args [][]byte) ([][]byte, error) {return nil, cmn.NewError("Value size is not 1")// Compute the root hash and assume it is valid.// The caller checks the ultimate root later.root := op.Proof.ComputeRootHash()err := op.Proof.Verify(root)return nil, cmn.ErrorWrap(err, "computing root hash")// XXX What is the encoding for keys?// We should decode the key depending on whether it's a string or hex,// maybe based on quotes and 0x prefix?err = op.Proof.VerifyItem([]byte(op.key), value)return nil, cmn.ErrorWrap(err, "verifying value")return [][]byte{root}, nil
为通过上述步骤1中 MerkleProof.validateMerkleProof
的验证 ,上述代码需要满足两点要求- 首先, 步骤5中函数
func (op IAVLValueOp) Run
返回的 return [][]byte{root}, nil
结果需与步骤4函数 func (poz ProofOperators) Verify
中的 args[0]
参数匹配,即 !bytes.Equal(root, args[0])
。 - 其次,代码
err := op.Proof.Verify(root)
与 err = op.Proof.VerifyItem([]byte(op.key), value)
的返回错误必须为nil
(7) 为了满足上述要求,关键在于函数 op.Proof.ComputeRootHash()
in func (op IAVLValueOp) Run
计算所得的root
是不可伪造的。因此,我们进一步深入函数 op.Proof.ComputeRootHash()
进行细节追踪:首先跳转至 func (proof *RangeProof) ComputeRootHash()
再跳转至 func (proof *RangeProof) _computeRootHash()
随后调用函数_computeRootHash()
中的闭包 func(path PathToLeaf, rightmost bool)
最后调用到闭包中的hash = (pathWithLeaf{Path: path, Leaf: nleaf,}).computeRootHash()
上述调用过程细节如下,有兴趣的读者可以进一步阅读代码:func (proof *RangeProof) ComputeRootHash() []byte {rootHash, _ := proof.computeRootHash()func (proof *RangeProof) _computeRootHash() (rootHash []byte, treeEnd bool, err error) {if len(proof.Leaves) == 0 {return nil, false, cmn.ErrorWrap(ErrInvalidProof, "no leaves")if len(proof.InnerNodes)+1 != len(proof.Leaves) {return nil, false, cmn.ErrorWrap(ErrInvalidProof, "InnerNodes vs Leaves length mismatch, leaves should be 1 more.")// Start from the left path and prove each leaf.// shared across recursive callsvar leaves = proof.Leavesvar innersq = proof.InnerNodesvar COMPUTEHASH func(path PathToLeaf, rightmost bool) (hash []byte, treeEnd bool, done bool, err error)// rightmost: is the root a rightmost child of the tree?// treeEnd: true iff the last leaf is the last item of the tree.// Returns the (possibly intermediate, possibly root) hash.COMPUTEHASH = func(path PathToLeaf, rightmost bool) (hash []byte, treeEnd bool, done bool, err error) {nleaf, rleaves := leaves[0], leaves[1:]// We're not done yet (leaves left over). No error, not done either.// Technically if rightmost, we know there's an error "left over leaves// -- malformed proof", but we return that at the top level, below.return hash, false, false, nilrootHash, treeEnd, done, err := COMPUTEHASH(path, true)return nil, treeEnd, cmn.ErrorWrap(err, "root COMPUTEHASH call")return nil, treeEnd, cmn.ErrorWrap(ErrInvalidProof, "left over leaves -- malformed proof")return rootHash, treeEnd, nil
(8) 因此,从函数func (pwl pathWithLeaf) computeRootHash()
中我们可以发现,步骤6中所述 root
仅与IAVL树的最左侧叶子节点及其路径级联的哈希相关。// `computeRootHash` computes the root hash with leaf node.// Does not verify the root hash.func (pwl pathWithLeaf) computeRootHash() []byte {leafHash := pwl.Leaf.Hash()return pwl.Path.computeRootHash(leafHash)
为便于理解,我们给出了一个IAVL树的示意图。代码层面的理解可以参考samczsun's code(https://gist.github.com/samczsun/8635f49fac0ec66a5a61080835cae3db#/)。
值得一提,上图中IAVL树中的数据类型在实际的结构体RangeProof
定义中的对应关系如下// You don't need the right path because// it can be derived from what we have.LeftPath PathToLeaf `json:"left_path"`InnerNodes []PathToLeaf `json:"inner_nodes"`Leaves []proofLeafNode `json:"leaves"`rootHash []byte // valid iff rootVerified is truetreeEnd bool // valid iff rootVerified is true
因此,我们可以初步判断,在IAVL树的最左侧叶子节点及其路径的哈希级联过程中存在漏洞。核心的漏洞
(9) 最后,我们在代码库github.com/tendermint/iavl@v0.12.0/proof_path.go
相关文件中,发现了中间节点哈希级联运算函数的漏洞。值得一提,尽管在BSC中调用的版本是v0.12.0,我们发现在该库的最新版本实现中仍然存在相应问题。无需担心的是,相关问题已经有热心开发者通过issue#579反馈给tendermint的相关开发团队。func (pin proofInnerNode) Hash(childHash []byte) []byte { // Where the bug is locatederr = amino.EncodeByteSlice(buf, childHash)err = amino.EncodeByteSlice(buf, pin.Right)err = amino.EncodeByteSlice(buf, pin.Left)err = amino.EncodeByteSlice(buf, childHash)hasher.Write(buf.Bytes())
结合我们的图例,在上述函数 func (pin proofInnerNode) Hash(childHash []byte)
中,目标中间节点的左孩子 len(pin.Left)
长度不为0,根据代码逻辑,理应进入到else
分支。在else
分支中,我们发现其计算仅涵盖了pin.Left
与childHash
,而 pin.Right
并未纳入哈希计算中。因此,尽管一个恶意的节点被插入到IAVL树的证明中,其IAVL树的根哈希校验依旧可以通过,最终导致了此次攻击的发生。这次针对跨链桥的攻击比较特殊:往常桥出现问题往往是智能合约编写或者升级出现漏洞,而这次是因为区块链底层以来的库出现问题。
本次有问题的库为 github.com/cosmos/iavl
,使用该库的项目包括 cosmos生态的核心组件 cosmos-sdk , IBC 等. 因此理论上使用cosmos-sdk构建的的项目、与cosmos跨链的项目都有可能受到影响,下文我们将分析是否有实际的攻击可能性。IBC协议:IBC协议为COSMOS生态之间跨链的标准。IBC协议在跨链时,会使用 vector commitment 来验证源链的交易确实发生。IBC协议中通过 ics23
规定了可使用的向量证明,其中就包括了 iavl树的证明。因此我们需要调查 github.com/cosmos/iavl
的错误实现是否引入 ics23
的实现中。ics23
实现为ics-23-go 和 [confio/ics23](https://github.com/confio/ics23/blob/a4daeb4c24ce1be827829c0841446abc690c4f11/go/proof.go#L122。这里的实现没有使用iavl库,因此没有收到影响。Cosmos-SDK
Cosmos-SDK 使用 IAVL+ 树来存储状态 , cosmos-sdk轻客户端使用 IAVL+ 树来证明某个状态的存在或者不存在。 Cosmos-SDK 提供了一个 工具将 IAVL树的证明转换为 ics23 Proof . 然而,没有证据表明SDK直接使用IAVL树的证明。所以cosmos-sdk并没有受到影响。
Gravity bridge使用多签来做Evm与COSMOS之间的跨链,因此不受本次攻击影响。EVMOS
EVMOS使用EVM上的 simple merkle 做跨链,不受影响。结论
目前我们没有发现可能的攻击路径 . 然而直接使用 github.com/cosmos/iavl
库做验证的项目依旧可能存在被攻击的风险(即使已经升级SDK)。小心!
BHBA_DAO是一个由北航学生组成的去中心化学生自治组织。我们的愿景是为区块链爱好者提供一个学习交流的平台。我们将通过区块链科普活动、区块链行业研究、区块链技术开发活动等丰富多彩的活动帮助在校学生了解区块链生态,掌握区块链技术。
作者:Tofu | CyberPigeon| Beihai